💡 今日學習目標:把「使用者登入之後拿到的那張憑證」當成一條有五個關卡的生命線來看,逐關掌握 Session Cookie 的安全屬性、JWT 的簽章驗證陷阱,以及 2025 年最新的密碼政策該怎麼訂。
Day 17 我們花了整篇處理「密碼該怎麼存」。但你有沒有想過:使用者登入成功之後,那組密碼就再也用不到了。
接下來一整個小時、一整天,伺服器認的不是密碼,而是登入當下發給他的那張憑證(Session ID 或 JWT Token)。
換句話說:你把密碼保護得再好,只要那張憑證能被偷走或偽造,前面所有的功夫都白費了。
這就是 A07:2025 認證失效 (Authentication Failures)。它在 2025 年名次沒有變動,仍然是第 7 名,只是名稱從「Identification and Authentication Failures」精簡了。但它的數字一點都不小:涵蓋 36 個 CWE、最高發生率 15.80%、累計出現 1,120,673 次,是十大類別中出現次數相當前段的一類。
大部分文章講到這個主題,就是給你「Cookie 三件套」加上「JWT 要驗簽章」。那確實是對的,但那只涵蓋了五分之二。

請特別注意第 1 關與第 5 關,它們是最常被整篇跳過、卻最常出事的兩關。我們一關一關來。
這一關的攻擊叫 Session Fixation(會話固定),運作方式是反過來的:攻擊者不是偷你的 Session ID,而是先發一組他自己知道的給你。
1. 攻擊者先造訪網站,拿到一組 Session ID:abc123
2. 他把 https://例子.com/?sessionid=abc123 這種連結傳給受害者
3. 受害者點進去、輸入帳密、登入成功
4. 如果伺服器「沿用」了 abc123 這組 ID,
攻擊者手上那組 abc123 現在就是一組已登入的管理員身分了
防守方式非常簡單,一行就解決:
Function OnLoginSuccess(user):
Session.Regenerate() // ← 關鍵:丟掉舊 ID,產生全新的一組
Session.Set("user_id", user.Id)
各語言的寫法:PHP 是 session_regenerate_id(true)、ASP.NET 要手動 abandon 後重建、Express 用 req.session.regenerate()。
🔑 為什麼這一關最常被漏掉? 因為漏了它,功能完全正常。使用者登入得進去、什麼錯誤訊息都不會有,測試也會通過。它是那種「只有攻擊者會發現」的漏洞,而這正是 Day 01 講的惡意路徑(Evil Path):你只測了「使用者會怎麼用」,沒測「攻擊者會怎麼用」。
❌ 不安全
Set-Cookie: session_id=abc123xyz; Path=/
✅ 安全
Set-Cookie: __Host-session=abc123xyz; Path=/; Secure; HttpOnly; SameSite=Lax
| 屬性 | 作用 |
|---|---|
HttpOnly |
JavaScript 無法透過 document.cookie 讀取。即使網站有 XSS,攻擊者也偷不走它 |
Secure |
只在 HTTPS 連線傳送,明文連線一律不帶 |
SameSite=Lax |
從別的網站發起的請求不會帶出這個 Cookie,大幅降低 CSRF |
__Host- 前綴 |
瀏覽器層級的強制保險 |
__Host- 前綴值得多講一句這是很多人沒用過、但成本為零的一招。當 Cookie 名稱以 __Host- 開頭,瀏覽器會強制檢查:必須有 Secure、必須來自 HTTPS、不可以有 Domain 屬性、Path 必須是 /。任何一項不符,瀏覽器直接拒收。
好處是:它讓你不可能不小心設錯。而且因為禁止 Domain 屬性,子網域就無法覆寫這個 Cookie,這擋掉了一整類「從一個被攻陷的子網域打主站」的攻擊。
SameSite 不等於沒有防護。現代瀏覽器(Chrome 80 之後)會預設當成 Lax。但別依賴預設值,不同瀏覽器行為不一致,明確寫出來。SameSite=None 一定要配 Secure,否則瀏覽器會直接丟棄這個 Cookie。這是很多人在做跨站嵌入功能時卡住的原因。// ❌ 錯誤:只解碼,不驗簽
Function HandleAPIRequest(req):
token = req.GetHeader("Authorization")
payload = JWT.DecodePayloadWithoutVerification(token) // 🚨
Return ProcessRequest(payload.UserId)
這段為什麼致命?因為 JWT 的 Payload 只是 Base64 編碼,不是加密。任何人都可以把 Token 貼到 jwt.io 上看內容、把 "role": "user" 改成 "role": "admin"、重新編碼送出。沒驗簽章 = 讓使用者自己填寫自己的權限。
⚠️ 順帶一提:既然 Payload 是任何人都看得到的,就不要把敏感資料放進去。身分證號、內部系統路徑、其他使用者的 email,放進 JWT 等於直接公開。
"alg": "none"JWT 規格允許 alg 設成 none,表示「這個 Token 沒有簽章」。早期有不少函式庫真的會照著做,攻擊者把 header 改成 {"alg":"none"}、把簽章欄位清空,就通關了。
這一個更精巧,也是實務上真正被利用的手法:
現在假設你的伺服器這樣寫:JWT.Verify(token, publicKey),把公鑰傳進一個通用的驗證函式。攻擊者做的事情是:
alg 從 RS256 改成 HS256
伺服器收到後,看到 alg: HS256,就把手上那把公鑰當成 HMAC 密鑰去驗,結果當然通過。
🔑 問題的根源是:伺服器相信了 Token 自己宣稱的演算法。而那個 header 是攻擊者可以隨意修改的。
// ✅ 安全:演算法白名單寫死在程式碼裡
Function HandleAPIRequest(req):
header = req.GetHeader("Authorization")
If NOT header.StartsWith("Bearer "):
Return Error(401)
token = header.Substring(7)
// 關鍵:AllowedAlgs 是我們自己指定的,不從 token 的 header 讀
claims = JWT.VerifyAndDecode(token, key,
AllowedAlgs = ["RS256"], // 只接受這一種
ExpectedAudience = "my-api",
ExpectedIssuer = "https://auth.example.com")
If claims.IsExpired():
Return Error(401, "Token Expired")
Return ProcessRequest(claims.UserId)
三個重點:演算法白名單寫死、驗 exp 過期時間、驗 aud 與 iss(確認這張票是發給「我這個服務」的,而不是同一個授權中心發給隔壁系統的票被拿來用)。
Session 的登出很單純:伺服器端把那筆 Session 資料刪掉,下次拿舊 ID 來就查無此人。
JWT 呢?JWT 是無狀態的:它一旦發出去,伺服器就管不了它了。
只要簽章有效、exp 還沒到,它就是有效的。使用者按了登出?Token 還能用。發現帳號被盜要停權?Token 還能用。管理員被降權了?他手上那張寫著 role: admin 的 Token 還是能用,直到過期為止。
這不是實作瑕疵,這是 JWT 的設計本質。實務上的處理方式是:
🧭 所以到底該用哪一個? 這是這一篇最實際的問題:
- 一般的 Web 應用程式(有前端頁面、有登入登出) ➔ 老老實實用伺服器端 Session。它能立即撤銷、能管理閒置逾時、放在
HttpOnlyCookie 裡也不怕 XSS。- 服務對服務、微服務之間、短效期的 API 存取 ➔ JWT 很適合,因為它的價值就在於「驗證時不必回頭查資料庫」。
很多團隊選 JWT 是因為它看起來比較現代,而不是因為他們真的需要無狀態。 而代價就是登出功能形同虛設。這是 Day 04 講的「冰山下的成本」:你以為選了一個技術,其實是選了一整組後續要處理的問題。
另外別忘了閒置逾時:使用者在公用電腦上登入後直接關掉瀏覽器,Session 應該在一段時間後自動失效,而不是等到瀏覽器關閉才處理。
A07 不只談憑證,也談密碼政策。而這裡有一個很多人還不知道的大轉彎。NIST 在 2025 年 7 月定稿的 SP 800-63B-4 裡,用了強制性的 SHALL NOT:
| 規定 | 內容 |
|---|---|
| SHALL NOT | 不得要求使用者定期更換密碼(除非有證據顯示已外洩) |
| SHALL NOT | 不得強制字元組合規則(不得要求「必須含大寫、數字、特殊符號」) |
| SHALL | 單一因素登入時,密碼最少 15 個字元;有搭配 MFA 時可放寬到 8 個字元 |
| SHOULD | 至少允許 64 個字元的長度上限 |
| SHALL | 新密碼必須比對外洩密碼清單,整組比對、不做部分比對 |
看到這裡,你可能會想到自己公司那條「每 90 天強制換密碼、必須含大小寫與特殊符號」的規定。那條規定按照 2025 年的標準,是明確被禁止的做法。
理由也很直白:強制輪換讓使用者從 Password1! 換成 Password2!;組合規則讓大家用同樣可預測的替換方式(a➔@、o➔0)。這兩條規則都在製造「看起來很複雜、實際上很好猜」的密碼。
🛡️ 而 OWASP 對 A07 排在第一位的建議是:導入 MFA。 上面所有的密碼規則加起來,防護力都比不上多一道因素。如果你的專案只能做一件事,做這件。
最後,登入 API 一定要有速率限制。OWASP 明文把「無法阻擋自動化的憑證填充(Credential Stuffing)與暴力破解」列為 A07 的第一條判斷指標。沒有限流,前面所有防守都可以被慢慢磨開。
alg 是攻擊者可以改的,所以演算法白名單必須寫死在你的程式碼裡。這跟 Day 18 的參數化是同一個精神:結構由我決定,資料才由對方提供。💬 明日預告:【Day 21】【動手做】SSRF 伺服端請求偽造:為什麼 2025 把它併進了 A01
明天是階段三的最後一天。我們要看一個很特別的案例:一個獨立存在了四年的類別,在 2025 年被 OWASP 收編了。而理解它為什麼被收編,比背下它的防禦手法更有價值。